Document React failure and Vue/Vue Vapor PSP benchmarks - #6
Draft
doodlewind wants to merge 8 commits into
Draft
Conversation
8 tasks
doodlewind
added a commit
that referenced
this pull request
Jul 30, 2026
…ly -O0 QuickJS builds Real-hardware testing showed hero-octane under 1 fps. Two root causes: 1. Every PSP QuickJS build was -O0: CRATE_CC_NO_DEFAULTS=1 drops the cc crate's synthesized flags including build.rs opt_level(), and the pin predated quickjs-rs PR #6 anyway. -O2 now lives in TARGET_CFLAGS and libquickjs-sys is repinned to master (ba5bdd0d) registry-wide (psp + vita Cargo, psp/symbian toolchain manifests, pin tests). 2. An Octane state commit replays the whole root regardless of where the state lives (~2-4ms desktop, engine-bound on PSP), so per-frame state is unaffordable there. Continuous motion now rides the native channels: sprite atlases (hero/gallery/library spinners via sprites.json + a new spinner-atlas.svg), baked keyframe timelines (music equalizer, apps/music/pocket.config.ts), animate()/jump() one-shots (stats systems reveal), ref-based phase timers (notifications, library loading), and a new setTextContent() escape hatch - the text-shaped sibling of animate() - for count-ups and percentages (stats tiles, music progress). useSpriteAnimation also stops committing state on ticks that do not change the visible sprite frame. PPSSPP avg frame work, before -> after (budget 16.7ms): hero 387.8 -> 6.5ms, stats 279.9 -> 9.2ms, music 282.7 -> 12.9ms, notifications 271.2 -> 13.8ms, library 50.8 -> 4.9ms, settings 27.4 -> 14.5ms, cards 9.7 -> 6.6ms. All three frameworks' e2e suites pass; only the six goldens whose pixels legitimately changed were re-baselined. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
doodlewind
added a commit
that referenced
this pull request
Jul 30, 2026
* feat(framework): add Octane as the third first-class framework Octane (React's programming model, compiled) joins Solid and Vue Vapor: universal-driver renderer over the native tree, full 15-component surface, use-prefixed frame hooks, app.octane.tsx variants for all 8 demos, playground in-browser compilation, byte-exact PPSSPP goldens, and a three-framework PPSSPP benchmark (docs/bench/) per the PR #6 methodology. The PSP host gains an arena-pressure GC; framework=octane builds stub octane/profiling to dodge a WeakMap pinning pathology in the pinned QuickJS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(octane): move continuous motion onto native channels; fix silently -O0 QuickJS builds Real-hardware testing showed hero-octane under 1 fps. Two root causes: 1. Every PSP QuickJS build was -O0: CRATE_CC_NO_DEFAULTS=1 drops the cc crate's synthesized flags including build.rs opt_level(), and the pin predated quickjs-rs PR #6 anyway. -O2 now lives in TARGET_CFLAGS and libquickjs-sys is repinned to master (ba5bdd0d) registry-wide (psp + vita Cargo, psp/symbian toolchain manifests, pin tests). 2. An Octane state commit replays the whole root regardless of where the state lives (~2-4ms desktop, engine-bound on PSP), so per-frame state is unaffordable there. Continuous motion now rides the native channels: sprite atlases (hero/gallery/library spinners via sprites.json + a new spinner-atlas.svg), baked keyframe timelines (music equalizer, apps/music/pocket.config.ts), animate()/jump() one-shots (stats systems reveal), ref-based phase timers (notifications, library loading), and a new setTextContent() escape hatch - the text-shaped sibling of animate() - for count-ups and percentages (stats tiles, music progress). useSpriteAnimation also stops committing state on ticks that do not change the visible sprite frame. PPSSPP avg frame work, before -> after (budget 16.7ms): hero 387.8 -> 6.5ms, stats 279.9 -> 9.2ms, music 282.7 -> 12.9ms, notifications 271.2 -> 13.8ms, library 50.8 -> 4.9ms, settings 27.4 -> 14.5ms, cards 9.7 -> 6.6ms. All three frameworks' e2e suites pass; only the six goldens whose pixels legitimately changed were re-baselined. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(psp): XMB cover art for all eight showcase demos; refresh bench + blog to post-fix numbers - tools/gen-demo-covers.ts bakes the pocket app-family cover convention for every showcase demo: ICON0 as a geometry-drawn family tile, PIC1 as a real sim-pumped frame of the app with the XMB legibility dim. apps/<demo>/psp/ fragments apply to every framework build of that demo. - docs/bench: archive the 2026-07-30 post-optimization dataset (geomean avg_work octane 1.66x vs solid, was 15.58x; all 21 cells inside the 16.7ms budget) alongside the pre-fix 07-29 dataset. - blog: add "The hardware said no" chapter (the -O0-since-2021 discovery, replay-scope-independence, the native-channel rule, per-demo before/after), refresh the benchmark section to the new dataset with a linear-scale chart, and re-count byte-identical goldens (14/23). - playground compiler-entry merges the music demo's equalizer keyframes into the playground-wide animation theme. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(blog): rewrite the Octane post's index blurb for outside readers No repo-internal references, leads with the machine and the three findings; post dated to its publish day. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(compiler): hard-error on <g transform> in bake-svg; flatten the spinner atlas The svg baker scans shapes flat, so the spinner atlas's per-cell <g transform="translate(...)"> was silently dropped - all eight frames baked into cell 0 and cells 1-7 shipped blank. On hardware the spinner sat invisible and "flashed" once per cycle when the frame index wrapped through the one painted cell; the golden re-baseline had baked the bug in. The atlas now carries absolute per-cell circle coordinates (no groups), and bake-svg refuses <g transform> loudly instead of mis-baking. All four affected goldens re-baked and visually verified: the spinner is present at a distinct phase in every capture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(blog): record the hardware confirmation and the blank-atlas incident The post-fix builds are verified on the same physical PSP that failed the first run; the sprite-cycling bullet now carries the honest beat about the <g transform> atlas bake and the golden re-baseline that hid it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
doodlewind
added a commit
that referenced
this pull request
Aug 6, 2026
…k XMB covers (#227) * feat(framework): add Octane as the third first-class framework Octane (React's programming model, compiled) joins Solid and Vue Vapor: universal-driver renderer over the native tree, full 15-component surface, use-prefixed frame hooks, app.octane.tsx variants for all 8 demos, playground in-browser compilation, byte-exact PPSSPP goldens, and a three-framework PPSSPP benchmark (docs/bench/) per the PR #6 methodology. The PSP host gains an arena-pressure GC; framework=octane builds stub octane/profiling to dodge a WeakMap pinning pathology in the pinned QuickJS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(octane): move continuous motion onto native channels; fix silently -O0 QuickJS builds Real-hardware testing showed hero-octane under 1 fps. Two root causes: 1. Every PSP QuickJS build was -O0: CRATE_CC_NO_DEFAULTS=1 drops the cc crate's synthesized flags including build.rs opt_level(), and the pin predated quickjs-rs PR #6 anyway. -O2 now lives in TARGET_CFLAGS and libquickjs-sys is repinned to master (ba5bdd0d) registry-wide (psp + vita Cargo, psp/symbian toolchain manifests, pin tests). 2. An Octane state commit replays the whole root regardless of where the state lives (~2-4ms desktop, engine-bound on PSP), so per-frame state is unaffordable there. Continuous motion now rides the native channels: sprite atlases (hero/gallery/library spinners via sprites.json + a new spinner-atlas.svg), baked keyframe timelines (music equalizer, apps/music/pocket.config.ts), animate()/jump() one-shots (stats systems reveal), ref-based phase timers (notifications, library loading), and a new setTextContent() escape hatch - the text-shaped sibling of animate() - for count-ups and percentages (stats tiles, music progress). useSpriteAnimation also stops committing state on ticks that do not change the visible sprite frame. PPSSPP avg frame work, before -> after (budget 16.7ms): hero 387.8 -> 6.5ms, stats 279.9 -> 9.2ms, music 282.7 -> 12.9ms, notifications 271.2 -> 13.8ms, library 50.8 -> 4.9ms, settings 27.4 -> 14.5ms, cards 9.7 -> 6.6ms. All three frameworks' e2e suites pass; only the six goldens whose pixels legitimately changed were re-baselined. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(psp): XMB cover art for all eight showcase demos; refresh bench + blog to post-fix numbers - tools/gen-demo-covers.ts bakes the pocket app-family cover convention for every showcase demo: ICON0 as a geometry-drawn family tile, PIC1 as a real sim-pumped frame of the app with the XMB legibility dim. apps/<demo>/psp/ fragments apply to every framework build of that demo. - docs/bench: archive the 2026-07-30 post-optimization dataset (geomean avg_work octane 1.66x vs solid, was 15.58x; all 21 cells inside the 16.7ms budget) alongside the pre-fix 07-29 dataset. - blog: add "The hardware said no" chapter (the -O0-since-2021 discovery, replay-scope-independence, the native-channel rule, per-demo before/after), refresh the benchmark section to the new dataset with a linear-scale chart, and re-count byte-identical goldens (14/23). - playground compiler-entry merges the music demo's equalizer keyframes into the playground-wide animation theme. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(blog): rewrite the Octane post's index blurb for outside readers No repo-internal references, leads with the machine and the three findings; post dated to its publish day. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(compiler): hard-error on <g transform> in bake-svg; flatten the spinner atlas The svg baker scans shapes flat, so the spinner atlas's per-cell <g transform="translate(...)"> was silently dropped - all eight frames baked into cell 0 and cells 1-7 shipped blank. On hardware the spinner sat invisible and "flashed" once per cycle when the frame index wrapped through the one painted cell; the golden re-baseline had baked the bug in. The atlas now carries absolute per-cell circle coordinates (no groups), and bake-svg refuses <g transform> loudly instead of mis-baking. All four affected goldens re-baked and visually verified: the spinner is present at a distinct phase in every capture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(blog): record the hardware confirmation and the blank-atlas incident The post-fix builds are verified on the same physical PSP that failed the first run; the sprite-cycling bullet now carries the honest beat about the <g transform> atlas bake and the golden re-baseline that hid it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(octane): upgrade to 0.1.26 scoped updates, move hero's count off the root octanejs/octane#578 (shipped in 0.1.26) makes a universal setState replay only the owning component's subtree instead of the whole root. It landed the day after we filed #574 with a repro, and our repro is now their regression benchmark (benchmarks/universal-leaf-update). The upgrade alone changes nothing on device: all 23 psp-octane goldens stay byte-exact, and every bench app's press cost is unmoved. The reason is on our side — octane scopes a replay to the nearest owner holding a committed range, and a root component has none, so state that lives in the root component still replays everything. Every demo keeps its interactive state in the root. Hero now demonstrates the shape the fix rewards: the counter moves into a CounterRow leaf, and the underline it also drives leaves the render path for the native jump() channel. Output is unchanged to the byte (all 23 goldens pass), while on PPSSPP: hero octane avg work 6.53 -> 3.64 ms max work 176.89 -> 37.80 ms hero solid avg work 3.66 ms max work 31.47 ms That is press-latency parity with Solid, from a 4.7x drop in the worst frame. The other six demos are unconverted; several of them hold state at the root legitimately (library's screen, stats' tab switch whole views) and need per-app judgement rather than a blanket refactor. Verified: full suite 8 groups 0 fail, tsc clean, 23/23 psp-octane goldens byte-exact, solid control unchanged (+0%) across the same bench run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(psp): per-framework XMB covers — the Octane twins name themselves A demo's framework twins were indistinguishable on a memory stick: both the Solid and the Octane build of `hero` packed the same icon and the same "PocketJS Hero" title, so the XMB gave you no way to tell which one you were about to launch. gen-demo-covers.ts grows --framework=<fw>, which bakes a variant set into apps/<demo>/psp/<fw>/ instead of the shared directory: the framework's name under the accent rule on the 144x80 tile, an accent-bordered badge in the PIC1 corner, and "(Octane)" appended to the PARAM.SFO title. PIC1 is rendered from that framework's own bundle. tools/psp.ts prefers <app>/psp/<framework>/ when it exists and otherwise keeps using the shared fragment, so default builds are untouched. All eight Octane demos get the variant set. Verified by unpacking the built PBPs: every one carries its Octane icon0/pic1 and an "(Octane)" SFO title. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Do not merge yet
This is still an experimental renderer migration PR and is intentionally kept as a draft with the
dont mergelabel.The highest-priority finding is still React: original/off-the-shelf React does not have a valid runnable PSP/PPSSPP renderer path in this investigation. The local
--engine=reactpath is a project-local React-shaped JSX compatibility shim and must not be counted as original React performance.Refs #5.
Correction: the earlier Vue Vapor benchmark was invalid
A PPSSPP capture showed the earlier Vue Vapor output was not a usable UI: it rendered a black screen with text collapsed at the top. That old Vapor chart/benchmark must not be used as evidence because it did not have feature parity.
This PR now includes the fix and a corrected benchmark run from the fixed state.
What changed
Vue,Vue Vapor, orSolid.ctx.attrs, which is where Vapor places undeclared props.createFor(...)usage now matches the Vapor runtime: render receives refs, key extraction receives raw item/key/index values.insert(...), fixing launcher/list crashes such ascannot read property 'anchor' of undefined.Validation
Result:
E2E OK; hero, cards, stats, library, settings, notifications, and music all produced complete PPSSPP capture windows and passed byte-exact comparison against the new Vue Vapor goldens.Launcher was validated separately because it is not in the E2E spec table:
launcher --engine=vue-vapor --captureproduced40/40frames;JS_Eval okand the frame loop;Vue Vaporframework label.All 8 Vue Vapor PSP memory-stick builds completed:
The resulting
psp-ui-*demos were copied to/Volumes/NO NAME/PSP/GAME, and source/destinationEBOOT.PBPSHA-256 hashes matched for all 8 demos.Corrected benchmark
PSP_SDK=/Users/evan/code/dreamcart/mipsel-sony-psp \ BENCH_PPSSPP_TIMEOUT=60 \ bun scripts/bench-ppsspp.ts \ --engines=vue-vapor,vue,solid \ --samples=7 \ --timeout=60 \ --bootstrap=5000Local report paths from the corrected run:
dist/bench/ppsspp-bench-2026-07-04T13-16-19-386Z.mddist/bench/ppsspp-bench-2026-07-04T13-16-19-386Z.jsondist/bench/ppsspp-bench-2026-07-04T13-16-19-386Z.raw.jsonlMetadata:
2026-07-04T13:17:55.024Z7676724ee5e02e824ea858673 (dirty)Matrix chart files: PNG · SVG
Benchmark headline
Lower is better for every metric.
eval_usboot_to_frame0_usavg_work_ushost_wall_msbundle_bytesAfter the feature-parity fix, Vue Vapor is no longer below Solid on average frame work. It is about
1.47xheavier than Solid across demos, but still much better than non-Vapor Vue: about0.39xthe frame work of non-Vapor Vue.Frame-work result by demo
Current conclusion
Original React remains not viable and must not receive credit through the local React-compatible shim.
Non-Vapor Vue runs, but misses the PSP smoothness bar in most demos and remains a negative result.
Vue Vapor now has a valid JSX + Tailwind feature-parity path, but the corrected numbers are mixed: it substantially improves over non-Vapor Vue frame work, yet it has worse startup/bundle size than Solid and higher cross-demo average frame work than Solid.